#import "ALSpinningPieChart.h"
#import "NSObject+BlockObservation.h"


#pragma mark Constants
static const CGFloat kALSpinningPieChartSelectionAzimuth = 3 * M_PI_2;
static const CGFloat kALSpinningPieChartMaxDragForClick = 3.0f;
static const CGFloat kALSpinningPieChartRotationDeceleration = -4 * M_PI;
static const CGFloat kALSpinningPieChartRotationTimerInterval = 1/30;
static const CGFloat kRotationDuration = 0.25f;
static const CGFloat kALSpinningPieChartMaxTimeSinceSpin = 0.25f;
static const NSUInteger kSpinningPieChartMaxTrackingCoordinates = 10;
static NSString* const kRotationStartDateKey = @"rotationStartDate";
static NSString* const kRotationInitialPolarAxisOffsetKey = @"rotationStartAzimuth";
static NSString* const kInitialAngularVelocityKey = @"initialAngularVelocity";
static NSString* const kPreviousRotationArcLengthsKey = @"previousRotationArcLengths";
static NSString* const kPreviousTimestampsKey = @"previousRotationTimestamps";


#pragma mark -
@interface ALSpinningPieChart ()
@property (nonatomic, assign) CGFloat initialPolarAxisOffset;
@property (nonatomic, assign) CGPoint initialPoint;
@property (nonatomic, retain) NSTimer *rotationTimer;
@property (nonatomic, retain) NSArray *previousRotationArcLengths;
@property (nonatomic, retain) NSArray *previousRotationTimestamps;
-(void)handleClickAtPoint_:(NSPoint)clickPoint inView:(SMRGraphView *)view;
-(void)staticRotateToPolarAxisOffset:(CGFloat)polarOffset;
-(SMR2DPolarCoordinateSet *)polarCoordinateSet_;
-(CGFloat)polarAxisOffsetForSelectionOfSymbol_:(SMR2DPolarChartSymbol *)selectedSymbol;
@end


#pragma mark -
@implementation ALSpinningPieChart

@synthesize initialPoint;
@synthesize initialPolarAxisOffset;
@synthesize rotationTimer;
@synthesize previousRotationTimestamps;
@synthesize previousRotationArcLengths;


#pragma mark -
#pragma mark Accessors

-(SMR2DPolarCoordinateSet *)polarCoordinateSet_ {
    return (SMR2DPolarCoordinateSet *)[self coordinateSet];
}


#pragma mark -
#pragma mark Selection

-(BOOL)allowsMultipleSelection {
    return NO;
}


#pragma mark -
#pragma mark Data

-(void)reloadValues {
    [super reloadValues];
    if ([self selectedSymbol] != nil || [[self chartSymbols] count] == 0)
        return;
    SMR2DPolarChartSymbol *firstSymbol = [[self chartSymbols] objectAtIndex:0];
    [self selectSymbol:firstSymbol extendSelection:NO];
    CGFloat polarAxis = [self polarAxisOffsetForSelectionOfSymbol_:firstSymbol];
    [[self polarCoordinateSet_] setPolarAxisOffset:polarAxis];
}


#pragma mark -
#pragma mark Rotation

-(void)decelerationRotationTimerDidFire:(NSTimer *)timer {
    NSDictionary *userInfo = [timer userInfo];
    CGFloat originalPolarAxisOffset = [[userInfo valueForKey:kRotationInitialPolarAxisOffsetKey] floatValue];
    NSDate *startDate = [userInfo valueForKey:kRotationStartDateKey];
    CGFloat initialAngularVelocity = [[userInfo valueForKey:kInitialAngularVelocityKey] floatValue];
    NSTimeInterval elapsedTime = fabs([startDate timeIntervalSinceNow]);
    CGFloat deceleration = kALSpinningPieChartRotationDeceleration * (initialAngularVelocity < 0 ? -1 : 1);
    CGFloat currentAngularVelocity = (deceleration * elapsedTime) + initialAngularVelocity;
    CGFloat arcLengthTraveled = (initialAngularVelocity + currentAngularVelocity) * elapsedTime / 2;
    CGFloat currentPolarAxisOffset = originalPolarAxisOffset + arcLengthTraveled;
    [self staticRotateToPolarAxisOffset:currentPolarAxisOffset];
    if (currentAngularVelocity * initialAngularVelocity < 0) {   // If no longer going in the same direction as initial velocity
        [timer invalidate];
        [self setRotationTimer:nil];
        SMR2DPolarChartSymbol *finalSymbol = [self symbolAtAzimuth:kALSpinningPieChartSelectionAzimuth];
        [self selectSymbol:finalSymbol extendSelection:NO];
        [self rotateSymbolIntoDisplayPosition:finalSymbol];
    }
}

-(void)staticRotateToPolarAxisOffset:(CGFloat)polarOffset {
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    [[self polarCoordinateSet_] setPolarAxisOffset:polarOffset];
    [CATransaction commit];
    SMR2DPolarChartSymbol *displaySymbol = [self symbolAtAzimuth:kALSpinningPieChartSelectionAzimuth];
    [self selectSymbol:displaySymbol extendSelection:NO];
}

-(void)rotateSymbolIntoDisplayPosition:(SMR2DPolarChartSymbol *)selectedSymbol {
    CGFloat polarAxis = [self polarAxisOffsetForSelectionOfSymbol_:selectedSymbol];
    [[self polarCoordinateSet_] setPolarAxisOffset:polarAxis];
}

-(CGFloat)polarAxisOffsetForSelectionOfSymbol_:(SMR2DPolarChartSymbol *)selectedSymbol {
    SMR2DPolarCoordinateSet *coordinates = (SMR2DPolarCoordinateSet *)[self coordinateSet];
    CGFloat polarAxis = kALSpinningPieChartSelectionAzimuth - [selectedSymbol azimuth] - ([selectedSymbol arcLength] / 2);
    CGFloat oldPolarAxis = [coordinates polarAxisOffset];
    CGFloat rotateDistance = polarAxis - oldPolarAxis;
    while (fabs(rotateDistance) > M_PI)
        rotateDistance += 2 * M_PI * (rotateDistance < 0 ? 1 : -1);
    return oldPolarAxis + rotateDistance;
}


#pragma mark -
#pragma mark Event handling

-(BOOL)startTracking:(NSEvent *)event at:(CGPoint)aPoint inGraphView:(SMRGraphView *)view {
    [[self rotationTimer] invalidate];
    [self setRotationTimer:nil];
    
    NSPoint viewPoint = [view convertPoint:[event locationInWindow] fromView:nil];
    [self setInitialPolarAxisOffset:[[self polarCoordinateSet_] polarAxisOffset]];
    [self setInitialPoint:viewPoint];
    [self setPreviousRotationTimestamps:[NSArray array]];
    [self setPreviousRotationArcLengths:[NSArray array]];
    return [super startTracking:event at:aPoint inGraphView:view];
}

-(BOOL)continueTracking:(NSEvent *)event at:(CGPoint)aPoint inGraphView:(SMRGraphView *)view {
    NSPoint viewPoint = [view convertPoint:[event locationInWindow] fromView:nil];
    CGFloat arcLength = [[self polarCoordinateSet_] arcLengthFromPoint:[self initialPoint] toPoint:viewPoint];
    CGFloat newPolarAxisOffset = [self initialPolarAxisOffset] + arcLength;
    [self staticRotateToPolarAxisOffset:newPolarAxisOffset];
    NSMutableArray *mutableArcLengths = [self mutableArrayValueForKey:kPreviousRotationArcLengthsKey];
    NSMutableArray *mutableTimestamps = [self mutableArrayValueForKey:kPreviousTimestampsKey];
    [mutableArcLengths addObject:[NSNumber numberWithFloat:arcLength]];
    [mutableTimestamps addObject:[NSNumber numberWithDouble:[event timestamp]]];
    if ([mutableArcLengths count] > kSpinningPieChartMaxTrackingCoordinates) {
        [mutableArcLengths removeObjectAtIndex:0];
        [mutableTimestamps removeObjectAtIndex:0];
    }
    return YES;
}

-(void)stopTracking:(NSEvent *)event at:(CGPoint)aPoint inGraphView:(SMRGraphView *)view {
    NSPoint viewPoint = [view convertPoint:[event locationInWindow] fromView:nil];
    NSPoint startPoint = [self initialPoint];
    CGFloat distanceFromStart = hypot(fabs(viewPoint.x - startPoint.x), fabs(viewPoint.y - startPoint.y));
    NSUInteger arcLengthCount = [[self previousRotationArcLengths] count];
    // If mouse movement is negligible or there aren't at least two tracking points (can't calculate velocity) ...
    if (distanceFromStart < kALSpinningPieChartMaxDragForClick || arcLengthCount < 2) {  
        [self handleClickAtPoint_:viewPoint inView:view];
        return;
    }
    
    NSTimeInterval currentTime = [event timestamp];
    NSTimeInterval lastTime = [[[self previousRotationTimestamps] lastObject] doubleValue];
    if ((currentTime - lastTime) > kALSpinningPieChartMaxTimeSinceSpin) {
        SMR2DPolarChartSymbol *finalSymbol = [self symbolAtAzimuth:kALSpinningPieChartSelectionAzimuth];
        [self selectSymbol:finalSymbol extendSelection:NO];
        [self rotateSymbolIntoDisplayPosition:finalSymbol];
    }
    
    CGFloat startArcLength = [[[self previousRotationArcLengths] objectAtIndex:0] floatValue];
    NSTimeInterval startTimestamp = [[[self previousRotationTimestamps] objectAtIndex:0] doubleValue];
    CGFloat sumVelocity = 0;
    for (NSUInteger trackingIndex = 1; trackingIndex < arcLengthCount; trackingIndex++) {
        CGFloat endArcLength = [[[self previousRotationArcLengths] objectAtIndex:trackingIndex] floatValue];
        NSTimeInterval endTimestamp = [[[self previousRotationTimestamps] objectAtIndex:trackingIndex] doubleValue];
        NSTimeInterval timeChange = endTimestamp - startTimestamp;
        if (timeChange > 0) {
            CGFloat arcLengthChanged = SMRNormalizedAzimuth(endArcLength) - SMRNormalizedAzimuth(startArcLength);
            if (fabs(arcLengthChanged) > M_PI) {
                arcLengthChanged += M_PI * (arcLengthChanged > 0 ? -2 : 2);
            }
            CGFloat velocity = arcLengthChanged / timeChange;
            sumVelocity += velocity;
        }
        startArcLength = endArcLength;
        startTimestamp = endTimestamp;
    }
    CGFloat angularVelocity = sumVelocity / arcLengthCount;
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    [userInfo setValue:[NSNumber numberWithFloat:angularVelocity] forKey:kInitialAngularVelocityKey];
    [userInfo setValue:[NSDate date] forKey:kRotationStartDateKey];
    CGFloat currentOffset = [[self polarCoordinateSet_] polarAxisOffset];
    [userInfo setValue:[NSNumber numberWithFloat:currentOffset] forKey:kRotationInitialPolarAxisOffsetKey];
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:kALSpinningPieChartRotationTimerInterval target:self selector:@selector(decelerationRotationTimerDidFire:) userInfo:userInfo repeats:YES];
    [self setRotationTimer:timer];
}

-(void)handleClickAtPoint_:(NSPoint)clickPoint inView:(SMRGraphView *)view {
    CALayer *rootLayer = [view layer];
    CGPoint chartPoint = [self convertPoint:clickPoint fromLayer:rootLayer];
    SMRPieChartSymbol *symbol = (SMRPieChartSymbol *)[self symbolAtPoint:chartPoint];
    [self selectSymbol:symbol extendSelection:NO];
    [self rotateSymbolIntoDisplayPosition:symbol];
}

@end
